文章同步發表至 Medium
最後一個想要建立的功能,是可以在網頁上搜尋景點附近的住宿位置。
- 身為一個使用者,我可以點選景點開啟詳細資訊,並查詢不同公尺範圍內的住宿。
- 身為一個使用者,我可以在地圖上看到我所查詢的住宿位置。
針對第一點,後端要做的事情有:
這裡要注意的地方是,Buffer 的建立在 TWD97 的座標系統底下才會是以公尺為單位,所以在建立 Buffer 和比對清單之前,要記得把景點和住宿的座標系統都換成 TWD97 喔。
[HttpGet("{meter:int}")]
public IActionResult SearchByScenicSpot(int meter, string scenicSpot)
{
// 利用景點代碼找景點的資料
var scenicSpot = _db.ScenicSpots.Find(scenicSpot);
// 處理找不到景點的情況
if (scenicSpot == null) throw new KeyNotFoundException($"找不到代碼為 {scenicSpotId} 的景點");
// !!!
// TWD97 座標系統底下的 Buffer 單位才是公尺
var twd97Geom = ConvertFromWgs84ToTwd97(scenicSpot.Geom);
var buffer = twd97Geom.Buffer(meter);
var hotelInfos = _hotelRepo.Get()
.Where(h => ConvertFromWgs84ToTwd97(h.Geom).Within(buffer))
.Select(s => new HotelInfo()
{
Id = s.Id,
Name = s.Name,
Telephone = s.Telephone,
Address = s.Address,
X = s.Geom.Centroid.X,
Y = s.Geom.Centroid.Y,
Type = s.Type ?? "未分類",
RoomCount = s.RoomCount,
LowestPrice = s.LowestPrice,
CeilingPrice = s.CeilingPrice,
Email = s.Email,
ParkingCount = s.ParkingCount
}).ToList();
var hotelsAroundScenicSpot = new HotelsAroundScenicSpot()
{
Infos = hotelInfos,
ScenicSpot = new ScenicSpotInfo()
{
X = scenicSpot.X,
Y = scenicSpot.Y,
},
};
return Ok(hotelsAroundScenicSpot);
}
// 詳細情形可以參考第 16 天的內容
private Geometry ConvertFromWgs84ToTwd97(Geometry wgs84Geom)
{
var wgs84Wkt = "GEOGCS[\"WGS84\",DATUM[\"WGS_1984\",SPHEROID[\"WGS84\",6378137,298.257223563,AUTHORITY[\"EPSG\",\"7030\"]],AUTHORITY[\"EPSG\",\"6326\"]],PRIMEM[\"Greenwich\",0,AUTHORITY[\"EPSG\",\"8901\"]],UNIT[\"degree\",0.01745329251994328,AUTHORITY[\"EPSG\",\"9122\"]],AUTHORITY[\"EPSG\",\"4326\"]]";
var twd97Wkt = "PROJCS[\"TWD97 / TM2 zone 121\",GEOGCS[\"TWD97\",DATUM[\"Taiwan_Datum_1997\",SPHEROID[\"GRS 1980\",6378137,298.257222101],TOWGS84[0,0,0,0,0,0,0]],PRIMEM[\"Greenwich\",0],UNIT[\"degree\",0.0174532925199433,AUTHORITY[\"EPSG\",\"9122\"]],AUTHORITY[\"EPSG\",\"3824\"]],PROJECTION[\"Transverse_Mercator\"],PARAMETER[\"latitude_of_origin\",0],PARAMETER[\"central_meridian\",121],PARAMETER[\"scale_factor\",0.9999],PARAMETER[\"false_easting\",250000],PARAMETER[\"false_northing\",0],UNIT[\"metre\",1],AXIS[\"Easting\",EAST],AXIS[\"Northing\",NORTH],AUTHORITY[\"EPSG\",\"3826\"]]";
var coordinateSystemFactory = new CoordinateSystemFactory();
var coordinateTransformationFactory = new CoordinateTransformationFactory();
var twd97 = coordinateSystemFactory.CreateFromWkt(Twd97Wkt);
var wgs84 = coordinateSystemFactory.CreateFromWkt(Wgs84Wkt);
var fromCoordinateSystems = coordinateTransformationFactory.CreateFromCoordinateSystems(wgs84, twd97);
return Transform(wgs84Geom, fromCoordinateSystems.MathTransform).Centroid;
}
static Geometry Transform(Geometry geom, MathTransform transform)
{
geom = geom.Copy();
geom.Apply(new MTF(transform));
return geom;
}
sealed class MTF : ICoordinateSequenceFilter
{
private readonly MathTransform _mathTransform;
public MTF(MathTransform mathTransform) => _mathTransform = mathTransform;
public bool Done => false;
public bool GeometryChanged => true;
public void Filter(CoordinateSequence seq, int i)
{
double x = seq.GetX(i);
double y = seq.GetY(i);
_mathTransform.Transform(ref x, ref y);
seq.SetX(i, x);
seq.SetY(i, y);
}
}
HTML 的部分只需要另外加上一顆按鈕就可以了:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>旅遊規劃</title>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.8.0/dist/leaflet.css"
integrity="sha512-hoalWLoI8r4UszCkZ5kL8vayOGVae1oxXe/2A4AO6J9+580uKHDO3JdHb7NzwwzK5xr/Fs0W40kiNHxM9vyTtQ=="
crossorigin=""/>
<link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@1.4.1/dist/MarkerCluster.Default.css">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.9.1/font/bootstrap-icons.css">
<style>
body{
height: 100vh;
}
#app, .row, #map{
height: 100%;
}
#search-by-county{
z-index: 1001;
top: 1%;
right: 0;
}
#search-hotel{
z-index: 1001;
top: 10%;
right: 0;
}
</style>
</head>
<body>
<div id="app" class="container-fluid">
<div class="row align-items-center position-relative">
<div id="map"></div>
<div id="search-by-county" class="position-absolute col-auto">
<button type="button" class="btn btn-lg btn-primary"
@click.prevent="searchScenicByCounty">
查詢
</button>
</div>
<div id="search-hotel" class="position-absolute col-auto">
<button type="button" class="btn btn-lg btn-primary"
@click.prevent="showList">
清單
</button>
</div>
</div>
</div>
<script src="https://unpkg.com/leaflet@1.8.0/dist/leaflet.js"
integrity="sha512-BB3hKbKWOc9Ez/TAwyWxNXeoV9c1v6FIeYiBieIWkpLjauysF18NzgR1MBNBXf8/KABdlkX68nAhlwcDFLGPCQ=="
crossorigin=""></script>
<script src="https://unpkg.com/leaflet.markercluster@1.4.1/dist/leaflet.markercluster.js"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script src="https://unpkg.com/vue@3"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>
</body>
</html>
至於 JavaScript 的部分,我們一樣利用 Sweet Alert 製作選單,只不過這次是兩層的,第一層是一個下拉式選單,可以選擇要查詢的景點:
第二層則是讓使用者輸入要查詢的範圍:
const app = Vue.createApp({
data(){
return{
map: null,
oldScenicSpotMarkers: null,
// 把景點和住宿的 markers 分開來
oldHotelMarkers: null,
// 下拉式選單的景點內容
list: null,
// 以查詢景點為中心畫出來的圓形 Buffer
circle: null
}
},
methods:{
initMap(){
// ...
},
resetScenicSpotMarkers(data){
// 把 this.oldMarkers 修改成 this.oldScenicSpotMarkers
},
resetHotelMarkers(data, meter){
// 如果已經查詢過就重置
if (this.oldHotelMarkers !== null) this.map.removeLayer(this.oldHotelMarkers);
if (this.circle !== null) this.map.removeLayer(this.circle);
this.oldHotelMarkers = L.markerClusterGroup();
// 設定住宿的 icon
let myIcon = L.icon({
iconUrl: './house-door.svg',
iconSize: [38, 95]
});
data.infos.map(ele => { this.oldHotelMarkers.addLayer(L.marker([ele.y, ele.x], {icon: myIcon})) });
this.map.addLayer(this.oldHotelMarkers);
// 設定圓形的 Buffer
this.circle = L.circle([data.scenicSpot.y, data.scenicSpot.x], {
color: '#ff6d6d',
fillColor: '#ff6d6d',
fillOpacity: 0.1,
radius: meter
}).addTo(this.map);
},
getScenicSpots(){
// ..
},
showList(){
let options = this.list.reduce((obj, item) => ({...obj, [item.id]: item.name}), {});
Swal.fire({
title: '尋找景點附近的住宿',
input: 'select',
inputOptions: options,
showCancelButton: true,
confirmButtonText: '下一步',
cancelButtonText: '取消'
}).then((result) => {
if (result.isConfirmed) {
// 如果點選下一步,繼續輸入搜尋範圍
Swal.fire({
title: '請輸入搜尋範圍(公尺)',
input: 'text',
showCancelButton: true,
confirmButtonText: '查詢',
cancelButtonText: '取消'
}).then((res2) => {
if (res2.isConfirmed) {
// 如果點擊查詢,call API
axios({
method: 'get',
url: `./api/Hotel/${res2.value}?scenicSpot=${result.value}`
}).then(res => {
this.resetHotelMarkers(res.data, res2.value);
}).catch(err => {
console.log(err);
})
}
})
}
})
},
searchScenicByCounty(){
// ...
},
},
mounted(){
this.initMap();
this.getScenicSpots();
}
});
app.mount('#app');
最後渲染出來的結果如下,粉紅色的房子是住宿,藍色的點則是原本的景點位置。